// Copyright 2020 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Package buildid provides the build id computation related functions.
//
// Build key:
// Build keys are autogenerated, monotonically decreasing integers.
// That is, when sorted by key, new builds are first.
// Build has no parent.
//
// Build id is a 64 bits integer.
//   - 1 highest order bit is set to 0 to keep value positive.
//   - 43 bits are 43 lower bits of bitwise-inverted time since
//     beginningOfTheWorld at 1ms resolution.
//     It is good for 2**43 / 365.3 / 24 / 60 / 60 / 1000 = 278 years
//     or 2010 + 278 = year 2288.
//   - 16 bits are set to a random value. Assuming an instance is internally
//     consistent with itself, it can ensure to not reuse the same 16 bits in two
//     consecutive requests and/or throttle itself to one request per
//     millisecond. Using random value reduces to 2**-15 the probability of
//     collision on exact same timestamp at 1ms resolution, so a maximum
//     theoretical rate of 65536000 requests/sec but an effective rate in the
//     range of ~64k qps without much transaction conflicts. We should be fine.
//   - 4 bits are 0. This is to represent the 'version' of the entity
//     schema.
//
// The idea is taken from Swarming TaskRequest entity:
// https://source.chromium.org/chromium/_/chromium/infra/luci/luci-py/+/a4a91d5e1e14b8b866b68b68bc1055b0b8ffef3b:appengine/swarming/server/task_request.py;l=1380-1404
package buildid

import (
	"context"
	"time"

	"go.chromium.org/luci/common/data/rand/mathrand"
)

const (
	timeResolution = time.Millisecond

	// BuildIDMax is the maximum theoretical build ID.
	// Based on time = beginningOfTheWorld, and max values for random and version bits.
	BuildIDMax = int64(0x7FFFFFFFFFFFFFFF)

	// buildIDTimeSuffixLen is the number of bits following the time in a build ID.
	buildIDTimeSuffixLen = 20

	// buildIDVersion is the version number of the build ID scheme.
	// Must not exceed 15 in the current build ID scheme since it's stored in four bits.
	buildIDVersion = 1
)

var (
	// beginningOfTheWorld is the earliest valid time encoded by build IDs.
	beginningOfTheWorld = time.Date(2010, 01, 01, 0, 0, 0, 0, time.UTC)
)

// NewBuildIDs generates the given number of build IDs in descending order.
func NewBuildIDs(ctx context.Context, t time.Time, n int) []int64 {
	// Build ID format [idTimeSegment] [random number] [buildIDVersion]
	// Generate n build IDs by getting a random number R, then using the n
	// integers in the range (R-n, R] as the random component while fixing
	// the time and version components (see package-level comment). Ensure
	// R is in [n-1, 2^16) so that the range (R-n, R] only contains
	// non-negative integers.
	r := mathrand.Intn(ctx, 65536-(n-1)) + (n - 1)
	base := idTimeSegment(t) | (int64(r) << 4) | buildIDVersion
	ids := make([]int64, n)
	for i := range ids {
		// Returned build IDs are in descending order:
		// [time] [R] [version]
		// [time] [R-1] [version]
		// [time] [R-2] [version]
		// ...
		// [time] [R-(n-1)] [version]
		ids[i] = base - int64(i*(1<<4))
	}
	return ids
}

// MayContainBuilds returns true if the time range can possibly contain builds.
// Zero low/high value means no boundary for low/high.
func MayContainBuilds(low, high time.Time) bool {
	switch {
	case !high.IsZero() && (high.Before(beginningOfTheWorld) || high.Equal(beginningOfTheWorld)):
		return false
	case !low.IsZero() && !high.IsZero() && high.Before(low):
		return false
	default:
		return true
	}
}

// IDRange converts a creation time range to the build id range.
// Low/high bounds are inclusive/exclusive respectively
// for both time and id ranges.
func IDRange(low, high time.Time) (int64, int64) {
	// Convert the inclusive low time bound to the exclusive high id bound.
	idHigh := idTimeSegment(low.Add(-timeResolution))
	// Convert the exclusive high time bound to the inclusive low id bound.
	idLow := idTimeSegment(high.Add(-timeResolution))
	return idLow, idHigh
}

// idTimeSegment constructs the build id bits: "0N{43}0{20}",
// where N is the bits converted from time.
// If time equals to beginningOfTheWorld, the id is 0x7FFFFFFFFFF00000.
// If the returned id is 0, it means its time is less than beginningOfTheWorld.
func idTimeSegment(t time.Time) int64 {
	delta := t.Sub(beginningOfTheWorld).Milliseconds()
	if delta < 0 {
		return 0
	}
	// Use bitwise negation to make sure build id is monotonically decreasing.
	// Thus the larger of the time, the smaller of the id.
	return (^delta & ((int64(1) << 43) - 1)) << buildIDTimeSuffixLen
}
